using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NUnit.Framework;

namespace ProjectEuler.Core
{
    public class Problem54 : IProjectEulerProblem
    {
        public struct Rank
        {
            public RankName RankNameItem;
            public int RankValue;
        }

        public enum RankName
        {
            HighCard = 1,
            OnePair = 2,
            TwoPairs = 3,
            ThreeOfAKind = 4,
            Straight = 5,
            Flush = 6,
            FullHouse = 7,
            FourOfAKind = 8,
            StraightFlush = 9,
            RoyalFlush = 10
        }

        public class Game
        {
            public Hand Player1 { get; set; }
            public Hand Player2 { get; set; }
            public int Winner
            {
                get
                {
                    var rank1 = Player1.GetRank;
                    var rank2 = Player2.GetRank;
                    if (rank1.RankNameItem == rank2.RankNameItem)
                    {
                        if (rank1.RankValue > rank2.RankValue)
                        {
                            return 1;
                        }
                        if (rank1.RankValue < rank2.RankValue)
                        {
                            return 2;
                        }

                        return HighestCards(Player1, Player2);
                    }
                    return rank1.RankNameItem > rank2.RankNameItem ? 1 : 2;
                }
            }

            private static int HighestCards(Hand player1, Hand player2)
            {
                var p1cards = player1.Cards.OrderByDescending(c => c.ValueNum).Select(c => c.ValueNum).ToArray();
                var p2cards = player2.Cards.OrderByDescending(c => c.ValueNum).Select(c => c.ValueNum).ToArray();
                for (int i = 0; i < 5; i++)
                {
                    if (p1cards[i] > p2cards[i]) return 1;
                    if (p1cards[i] < p2cards[i]) return 2;
                }
                throw new Exception("undefined winner: this exception should only be thrown if both players have identical hands");
            }
        }

        public class Card
        {
            public char Suit { get; set; }
            public char Value { get; set; }
            public int ValueNum
            {
                get
                {
                    if (Value >= '2' && Value <= '9')
                    {
                        return (int)char.GetNumericValue(Value);
                    }
                    switch (Value)
                    {
                        case 'T': return 10;
                        case 'J': return 11;
                        case 'Q': return 12;
                        case 'K': return 13;
                        case 'A': return 14;
                    }
                    return -1;
                }
            }
        }

        public class Hand
        {
            private readonly string _cards;
            public Hand(string cards)
            {
                _cards = cards;
            }

            public IEnumerable<Card> Cards
            {
                get
                {
                    var cardstrings = _cards.Split(' ');
                    return cardstrings.Select(cardstring => new Card { Suit = cardstring[1], Value = cardstring[0] });
                }
            }

            private Rank? _rank;
            public Rank GetRank
            {
                get
                {
                    if (_rank == null)
                    {
                        var rankName = FindRank();
                        var rankValue = FindRankValue(rankName);
                        _rank = new Rank { RankNameItem = rankName, RankValue = rankValue };
                    }
                    return _rank.Value;
                }
            }

            private int FindRankValue(RankName rankName)
            {
                if (rankName == RankName.RoyalFlush) return 14;     // always Ace

                if (rankName == RankName.StraightFlush) return Cards.Max(c => c.ValueNum);
                if (rankName == RankName.Flush) return Cards.Max(c => c.ValueNum);
                if (rankName == RankName.Straight) return Cards.Max(c => c.ValueNum);

                if (rankName == RankName.FourOfAKind) return GetValueN_of_a_Kind(4);
                if (rankName == RankName.FullHouse) return GetValueN_of_a_Kind(3);
                if (rankName == RankName.ThreeOfAKind) return GetValueN_of_a_Kind(3);
                if (rankName == RankName.OnePair) return GetValueN_of_a_Kind(2);
                if (rankName == RankName.TwoPairs)
                {
                    var pair1val = 0;
                    var pair2val = 0;
                    foreach (var card in Cards)
                    {
                        Card card1 = card;
                        if (Cards.Count(c => c.ValueNum == card1.ValueNum) == 2)
                        {
                            if (pair1val == 0) pair1val = card.ValueNum;
                            if (pair2val == 0) pair2val = card.ValueNum;
                        }
                        return pair1val > pair2val ? pair1val : pair2val;
                    }
                }

                // high card
                return Cards.Max(c => c.ValueNum);
            }

            private int GetValueN_of_a_Kind(int n)
            {
                foreach (var card in Cards)
                {
                    var cardToCheck = card;
                    if (Cards.Count(c => c.ValueNum == cardToCheck.ValueNum) == n) return card.ValueNum;
                }
                throw new ArgumentException("No set of " + n + " of a kind found");
            }

            private RankName FindRank()
            {
                // both the ordering and the immediate returning is important
                if (IsFlush() && IsRoyal()) return RankName.RoyalFlush;
                if (IsFlush() && IsStraight()) return RankName.StraightFlush;
                if (IsFourOfAKind()) return RankName.FourOfAKind;
                if (IsFullHouse()) return RankName.FullHouse;
                if (IsFlush()) return RankName.Flush;
                if (IsStraight()) return RankName.Straight;
                if (IsThreeOfAkind()) return RankName.ThreeOfAKind;
                if (IsTwoPair()) return RankName.TwoPairs;
                if (IsOnePair()) return RankName.OnePair;
                return RankName.HighCard;
            }

            public bool IsOnePair()
            {
                var two = false;
                foreach (var card in Cards)
                {
                    var cardToCheck = card;
                    if (Cards.Count(c => c.Value == cardToCheck.Value) == 2) two = true;
                }
                return two;
            }

            public bool IsTwoPair()
            {
                char pair1Value = 'x';
                foreach (var card in Cards)
                {
                    var cardToCheck = card;
                    if (Cards.Count(c => c.Value == cardToCheck.Value) != 2) continue;
                    if (pair1Value == 'x')
                    {
                        pair1Value = card.Value;
                    }
                    else
                    {
                        if (pair1Value != card.Value)
                        {
                            return true;
                        }
                    }
                }
                return false;
            }

            public bool IsThreeOfAkind()
            {
                var three = false;
                foreach (var card in Cards)
                {
                    var cardToCheck = card;
                    if (Cards.Count(c => c.Value == cardToCheck.Value) == 3) three = true;
                }
                return three;
            }

            public bool IsFullHouse()
            {
                return IsThreeOfAkind() && IsOnePair();
            }

            public bool IsFourOfAKind()
            {
                var card1 = Cards.First();
                var card2 = Cards.Last();
                return ((Cards.Count(c => c.ValueNum == card1.ValueNum) == 4) || (Cards.Count(c => c.ValueNum == card2.ValueNum) == 4));
            }

            public bool IsStraight()
            {
                var values = Cards.Select(c => c.ValueNum).OrderBy(c => c).ToArray();
                return (values[0] == values[1] - 1 && values[1] == values[2] - 1 && values[2] == values[3] - 1 && values[3] == values[4] - 1);
            }

            public bool IsRoyal()
            {
                var cards = Cards;
                var sortedcards = cards.Select(c => c.Value).OrderBy(c => c).ToArray();
                const string royal = "AJKQT";
                return string.Join("", sortedcards) == royal;
            }

            protected bool IsFlush()
            {
                return Cards.All(c => c.Suit == Cards.First().Suit);
            }
        }

        protected IEnumerable<Game> Games
        {
            get
            {
                var lines = File.ReadLines("GivenFiles\\poker.txt");
                return from line in lines
                       let player1 = new Hand(line.Substring(0, 14).Trim())
                       let player2 = new Hand(line.Substring(14, 15).Trim())
                       select new Game { Player1 = player1, Player2 = player2 };
            }
        }

        public int Number
        {
            get { return 54; }
        }

        public string Description
        {
            get { return "How many hands does Player 1 win?"; }
        }

        public string Answer
        {
            get
            {
                return Games.Count(g => g.Winner == 1).ToString();
            }
        }
    }

    [TestFixture]
    public class Problem54Tests : Problem54
    {
        [Test]
        public void There_should_be_one_game_per_line()
        {
            var numlines = File.ReadLines("GivenFiles\\poker.txt").Count();
            Assert.AreEqual(numlines, Games.Count());
        }

        [Test]
        public void Each_hand_should_have_five_cards()
        {
            foreach (var game in Games)
            {
                Assert.AreEqual(5, game.Player1.Cards.Count());
            }
        }

        [Test]
        public void Can_tell_IsStraight()
        {
            var hand = new Hand("TH 9D 6D 8C 7C");
            Assert.AreEqual(RankName.Straight, hand.GetRank.RankNameItem);
            hand = new Hand("TH 9D 5D 8C 7C");
            Assert.AreNotEqual(RankName.Straight, hand.GetRank.RankNameItem);
        }

        [Test]
        public void Can_tell_IsRoyalFlush()
        {
            var hand = new Hand("TD JD KD QD AD");
            Assert.AreEqual(RankName.RoyalFlush, hand.GetRank.RankNameItem);
            hand = new Hand("TH 9D 5D 8C 7C");
            Assert.AreNotEqual(RankName.RoyalFlush, hand.GetRank.RankNameItem);
        }

        [Test]
        public void Can_tell_IsFlush()
        {
            var hand = new Hand("8D JD KD 7D AD");
            Assert.AreEqual(RankName.Flush, hand.GetRank.RankNameItem);
            hand = new Hand("TH 9D 5D 8C 7C");
            Assert.AreNotEqual(RankName.Flush, hand.GetRank.RankNameItem);
        }

        [Test]
        public void Can_tell_IsFourOfAKind()
        {
            var hand = new Hand("JH JD JD QC JC");
            Assert.AreEqual(RankName.FourOfAKind, hand.GetRank.RankNameItem);
            hand = new Hand("TH 9D 5D 8C 7C");
            Assert.AreNotEqual(RankName.FourOfAKind, hand.GetRank.RankNameItem);
        }

        [Test]
        public void Can_tell_IsFullHouse()
        {
            var hand = new Hand("JH JD JC QH QC");
            Assert.AreEqual(RankName.FullHouse, hand.GetRank.RankNameItem);
            hand = new Hand("TH 9D 5D 8C 7C");
            Assert.AreNotEqual(RankName.FullHouse, hand.GetRank.RankNameItem);
        }

        [Test]
        public void Can_tell_IsThreeOfAKind()
        {
            var hand = new Hand("JH JD JC 5H 2C");
            Assert.AreEqual(RankName.ThreeOfAKind, hand.GetRank.RankNameItem);
            hand = new Hand("TH 9D 5D 8C 7C");
            Assert.AreNotEqual(RankName.ThreeOfAKind, hand.GetRank.RankNameItem);
        }

        [Test]
        public void Can_tell_IsTwoPair()
        {
            var hand = new Hand("JH JD KC KH 2C");
            Assert.AreEqual(RankName.TwoPairs, hand.GetRank.RankNameItem);
            hand = new Hand("TH 9D 5D 8C 7C");
            Assert.AreNotEqual(RankName.TwoPairs, hand.GetRank.RankNameItem);
        }

        [Test]
        public void Can_tell_IsOnePair()
        {
            var hand = new Hand("JH JD QC KH 2C");
            Assert.AreEqual(RankName.OnePair, hand.GetRank.RankNameItem);
            hand = new Hand("TH 9D 5D 8C 7C");
            Assert.AreNotEqual(RankName.OnePair, hand.GetRank.RankNameItem);
        }

        [Test]
        public void Sample_hand_1_player_2_wins()
        {
            var game = new Game();
            game.Player1 = new Hand("5H 5C 6S 7S KD");
            game.Player2 = new Hand("2C 3S 8S 8D TD");

            Assert.AreEqual(2, game.Winner);
        }

        [Test]
        public void Sample_hand_2_player_1_wins()
        {
            var game = new Game();
            game.Player1 = new Hand("5D 8C 9S JS AC");
            game.Player2 = new Hand("2C 5C 7D 8S QH");

            Assert.AreEqual(1, game.Winner);
        }

        [Test]
        public void Sample_hand_3_player_2_wins()
        {
            var game = new Game();
            game.Player1 = new Hand("2D 9C AS AH AC");
            game.Player2 = new Hand("3D 6D 7D TD QD");

            Assert.AreEqual(2, game.Winner);
        }

        [Test]
        public void Sample_hand_4_player_1_wins()
        {
            var game = new Game();
            game.Player1 = new Hand("4D 6S 9H QH QC");
            game.Player2 = new Hand("3D 6D 7H QD QS");

            Assert.AreEqual(1, game.Winner);
        }

        [Test]
        public void Sample_hand_5_player_1_wins()
        {
            var game = new Game();
            game.Player1 = new Hand("2H 2D 4C 4D 4S");
            game.Player2 = new Hand("3C 3D 3S 9S 9D");

            Assert.AreEqual(1, game.Winner);
        }
    }
}